package org.mtmi.ui.scenes;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import net.n3.nanoxml.IXMLElement;
import net.n3.nanoxml.IXMLParser;
import net.n3.nanoxml.IXMLReader;
import net.n3.nanoxml.StdXMLReader;
import net.n3.nanoxml.XMLException;
import net.n3.nanoxml.XMLParserFactory;

import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Shell;
import org.mtmi.io.ReaderError;
import org.mtmi.io.XMLReader;
import org.mtmi.io.XMLWriter;
import org.mtmi.ui.M;
import org.mtmi.ui.MouseInteractionManager;
import org.mtmi.ui.actions.IAction;
import org.mtmi.ui.controls.Control;
import org.mtmi.ui.controls.Menu;
import org.mtmi.ui.controls.curve.CurveManager;
import org.mtmi.ui.controls.listeners.ControlBoundsEvent;
import org.mtmi.ui.controls.listeners.ControlBoundsListener;
import org.mtmi.ui.controls.listeners.MenuListener;
import org.mtmi.ui.controls.listeners.MenuSelectionListener;
import org.mtmi.ui.scenes.actions.AddControlAction;
import org.mtmi.ui.scenes.actions.RemoveControlAction;

/**
 * This class is in charge of managing all scenes created.
 * @author jgout
 *
 */
public class SceneManager implements MenuListener, MenuSelectionListener
{

	private String scenes_directory; //directory to save/load scene file.
	private List<Scene> scenes;
	private int currentIndex;
	private StackLayout layout;
	private Shell parent;
	private boolean editing;
	private List<ISceneManagerListener> lis_mgr;
	
	private static SceneManager instance;
	
	//used by lazy guy like me to play with scene without starting TUIO simulator or ...
	private MouseInteractionManager mgrMouse;
	
	public static SceneManager getDefault() { return instance; }
	
	public  SceneManager(Shell shell) {
		if( instance!= null ) throw new IllegalStateException("SceneManager already instanciated"); //$NON-NLS-1$
		instance=this;
		parent = shell;
		scenes = new ArrayList<Scene>();
		currentIndex = 0;
		layout = new StackLayout();
		shell.setLayout(layout);
		editing = false;
	}
	
	public void setSceneDirectory( String dir ) {
		scenes_directory = dir;
	}
	
	/** @return the scene file directory */
	public String getSceneDirectory() { return scenes_directory; }
	
	/** @return the shell used to display scenes */
	public Shell getShell() { return parent; }
	
	private Point global_action_toolbar_location;
	
	public boolean isEditing() { return editing; }
	
	public void setEditing( boolean ed ) {
		editing = ed;
		for( Scene s : scenes ) {
			s.setEditing( ed );
		}
		
		if( editing ) {
			//note: toolbar will dispose it self
			final GlobalActionToolBar tb = new GlobalActionToolBar( getScene(), M.HORIZONTAL );
			tb.addControlBoundsListener( new ControlBoundsListener() {
				@Override
				public void controlBoundsChanged(ControlBoundsEvent event) {
					global_action_toolbar_location = tb.getLocation();
				}
			});
			if( global_action_toolbar_location !=null ) {
				tb.setLocation( global_action_toolbar_location );
			} else {
				//by default location if h-centere on top of scene
				Point ss = getScene().getSize();
				Point sb = tb.getSize();
				tb.setLocation( (ss.x-sb.x)/2, 5 );
			}
		} else {
			CurveManager.writeCurves();
			saveData();
		}
		notifyModeChanged();
	}
	
	/** @return the number of scenes */
	public int getSceneCount() { return scenes.size(); }
	
	/**
	 * Returns the current scene.
	 * @return the current scene
	 */
	public Scene getScene() {
		return scenes.get(currentIndex);
	}
	
	/**
	 * @return the scene currently loading in manager.
	 */
	public Scene[] getScenes() {
		return scenes.toArray( new Scene[ scenes.size() ] );
	}
	
	/** @return the index of given scene */
	public int getIndexOf( Scene s ) {
		return scenes.indexOf( s );
	}
	
	/**
	 * Tests whether the current scene is the first one.
	 * @return true if there is a scene before the current one and false if the current one is the first scene.
	 */
	public boolean hasPrevious() {
		return currentIndex > 0;
	}
	
	/** 
	 * Tests whether the current scene is the last or not.
	 * @return true if there is a scene after the current one and false otherwise.
	 */
	public boolean hasNext() {
		return currentIndex < scenes.size()-1;
	}
	
	/**
	 * Adds the given scene at the end of the scenes list.
	 * @param s a scene
	 */
	public void addScene(Scene s) {
		addScene( s, scenes.size() );
	}
	
	/**
	 * Adds the given scene at position (0..getSceneCount())
	 * @param s the scene to add
	 * @param index insert index (0..getSceneCount())
	 */
	public void addScene( Scene s, int index ) {
		if(!scenes.contains(s)) {
			Scene curr = scenes.size()==0 ? null : getScene(); //current scene
			scenes.add( index, s );
			s.setEditing( editing );
			notifySceneAdded( s );
			//current scene is not changed, but it index may be modified:
			if( curr==null ) {
				currentIndex = 0;
			} else {
				currentIndex = scenes.indexOf( curr );
			}
		}
		if(scenes.size() == 1) {
			//- set the first scene when 
			layout.topControl = scenes.get(0);
			
			mgrMouse = new MouseInteractionManager();
			mgrMouse.setScene( s );
			
			//this button must always be displayed whatever the mode is
			new GlobalSceneButton( s, DEFAULT_GSB_LOCATION );
		}
	}
	
	/**
	 * remove the current scene, except if it's the only one scene.
	 * If scene it's the current one, current scene is changed to next one.
	 * @param s
	 */
	public void removeScene( Scene s )
	{
		if( scenes.size()==1 ) return ; //must always keep one scene.
		int idx = scenes.indexOf( s );
		if( idx < 0 ) return ;
		Scene curr = getScene(); //current scene
		boolean is_current = idx == currentIndex;
		scenes.remove( s );
		notifySceneRemoved( s );
		if( is_current ) {
			if( currentIndex >= scenes.size() ) {
				currentIndex = scenes.size()-1;
			}
			changeScene( null );//old scene is null because removed
		} else {
			//current scene is not changed, but it index may be modified:
			currentIndex = scenes.indexOf( curr );
		}
	}
	
	
	/**
	 * Changes the current scene to the one given by its index (0 is the first one) in the scenes list.
	 * @param index an index to specify the scene to select 
	 * (index must be greater or equal to 0 and fewer than the number of scenes added to the manager).
	 * @return true if the change of scene has been successfully performed or false if index is out of bounds.
	 */
	public boolean setScene(int index) {
		if(index >= 0 && index < scenes.size() && index!=currentIndex ) {
			Scene old = getScene();
			currentIndex = index;
			changeScene( old );
			return true;
		}
		return false;
	}
	
	public boolean setScene( Scene s ) {
		int idx = scenes.indexOf( s );
		return setScene( idx );
	}
	
	/**
	 * Selects the next scene if it exists.
	 * @return true if the next scene has been selected or false if the 
	 * current scene is already the last one (no more next scene).
	 */
	public boolean nextScene() {
		if(hasNext()) {
			Scene old = getScene();
			currentIndex++;
			changeScene( old );
			return true;
		}
		return false;
	}
	
	/**
	 * Selects the previous scene if it exists.
	 * @return true if the previous scene has been selected or false if the 
	 * current scene is already the first one (no more previous scene).
	 */
	public boolean previousScene() {
		if(hasPrevious()) {
			Scene old = getScene();
			currentIndex--;
			changeScene( old );
			return true;
		} 
		return false;
	}
	
	private void changeScene( Scene old ) {
		Scene curr = getScene();
		layout.topControl = curr;
		parent.layout();
		mgrMouse.setScene( curr );
		parent.redraw();
		notifySceneChanged( old );
	}
	
	@Override
	public void menuClosed( Menu _menu ) {
	}

	@Override
	public void menuOpening( Menu _menu ) {
		_menu.setMenuEntryEnabled( 1001, getScene().hasSelection() );
	}
	
	@Override
	public void menuEntrySelected( int id ) {
		switch( id ) {
		case 1000: new AddControlAction().run(); break;
		case 1001: new RemoveControlAction().run(); break; 
		}
	}
	
	public void addSceneManagerListener( ISceneManagerListener lis ) {
		if( lis_mgr == null ) {
			lis_mgr = new ArrayList<ISceneManagerListener>();
		}
		lis_mgr.add( lis );
	}
	public void removeSceneManagerListener( ISceneManagerListener lis ) {
		if( lis_mgr != null ) {
			lis_mgr.remove( lis );
		}
	}
	private void notifyModeChanged() {
		if( lis_mgr != null ) {
			ISceneManagerListener lis[] =lis_mgr.toArray( new ISceneManagerListener[lis_mgr.size()] );
			for( ISceneManagerListener l : lis ) {
				l.sceneManagerModeChanged( editing );
			}
		}
	}
	private void notifySceneChanged( Scene old ) {
		if( lis_mgr != null ) {
			ISceneManagerListener lis[] =lis_mgr.toArray( new ISceneManagerListener[lis_mgr.size()] );
			Scene scene = getScene();
			for( ISceneManagerListener l : lis ) {
				l.sceneManagerSceneChanged( scene, old );
			}
		}
	}
	private void notifySceneAdded( Scene added ) {
		if( lis_mgr != null ) {
			ISceneManagerListener lis[] =lis_mgr.toArray( new ISceneManagerListener[lis_mgr.size()] );
			for( ISceneManagerListener l : lis ) {
				l.sceneManagerSceneAdded( added );
			}
		}
	}
	private void notifySceneRemoved( Scene old ) {
		if( lis_mgr != null ) {
			ISceneManagerListener lis[] =lis_mgr.toArray( new ISceneManagerListener[lis_mgr.size()] );
			for( ISceneManagerListener l : lis ) {
				l.sceneManagerSceneRemoved( old );
			}
		}
	}
	
	private GlobalSceneButton getGlobalSceneButton()
	{
		Scene s = getScene();
		if( s==null ) return null;
		for( Control c : s.getControls( true ) ) {
			if( c instanceof GlobalSceneButton ) {
				return (GlobalSceneButton)c;
			}
		}
		return null;
	}
	
	private static final int DEFAULT_GSB_LOCATION = M.TOP|M.LEFT;
	
	private static final String FILENAME = "manager.mgr"; //$NON-NLS-1$
	private static final String E_MANAGER = "manager"; //$NON-NLS-1$
	private static final String E_GLOBAL_SCENE_BUTTON = "globalSceneButton"; //$NON-NLS-1$
	private static final String E_GLOBAL_ACTION_TOOLBAR = "globalActionToolbar"; //$NON-NLS-1$
	private static final String E_SIDE_BUTTON = "sideButton"; //$NON-NLS-1$
	private static final String A_FLAGS = "flags"; //$NON-NLS-1$
	private static final String A_ACTION = "action"; //$NON-NLS-1$
	private static final String A_BOUNDS = "bounds"; //$NON-NLS-1$
	private static final String A_LOCATION = "location"; //$NON-NLS-1$
	
	/** if a scene directory is defined, save scene manager data in it file */
	public void saveData()
	{
		if( scenes_directory==null ) return ;
		
		Scene scene = getScene();
		if( scene==null ) return; //nothing to save
		
		GlobalSceneButton gsb = getGlobalSceneButton();
		
		ArrayList<GlobalSceneSideActionButton> sbs = new ArrayList<GlobalSceneSideActionButton>();
		for( Control c : scene.getControls( true ) ) {
			if( c instanceof GlobalSceneSideActionButton ) {
				sbs.add( (GlobalSceneSideActionButton)c );
			}
		}
		
		String fullname = scenes_directory+File.separator+FILENAME;
		try {
			FileOutputStream fos = new FileOutputStream( fullname );
			XMLWriter xp = new XMLWriter( fos );
			
			xp.startElement( E_MANAGER );
			
			//need to save location of gsb ?
			if( gsb!=null &&  (gsb.getFlags()&M.DIRECTION_MASK) != DEFAULT_GSB_LOCATION ) {
				int sl = gsb.getSideLocation();
				xp.addElement( E_GLOBAL_SCENE_BUTTON, A_FLAGS, sl, A_BOUNDS, gsb.getBounds(null) );
			}
			
			if( global_action_toolbar_location!=null ) {
				xp.addElement( E_GLOBAL_ACTION_TOOLBAR, A_LOCATION, global_action_toolbar_location );
			}
			
			for( GlobalSceneSideActionButton b : sbs ) {
				int sl = b.getSideLocation();
				String a = b.getAction().getClass().getName();
				xp.addElement( E_SIDE_BUTTON, A_FLAGS, sl, A_ACTION, a, A_BOUNDS, b.getBounds(null) );
			}
			
			xp.endElement( E_MANAGER);
			
		} catch( IOException x ) {
			x.printStackTrace(); //TODO a log file ???
		}
	}
	
	/** try to load data from manager's configuration file */
	public void loadData()
	{
		if( scenes_directory==null ) return ;
		Scene scene = getScene();
		//because SideButton require a scene:
		if( scene == null ) throw new Error("cannot load data file if there are no scene.");
		
		//as fileReader() require an URI: filename must not start with 'c:'..=> unknown protocol
		String fullname = "/"+scenes_directory+File.separator+FILENAME; //$NON-NLS-1$
		FileInputStream fis = null;
System.out.println("LOADING SceneManager cfg file: "+fullname);		
		try {
			
			XMLReader xr = new XMLReader();
			
			IXMLParser parser = XMLParserFactory.createDefaultXMLParser();
			fis = new FileInputStream( fullname );
			IXMLReader reader = new StdXMLReader( fis );

			parser.setReader(reader);
			IXMLElement e_mgr = (IXMLElement) parser.parse();
			
			if( !E_MANAGER.equals( e_mgr.getName() ) ) {
				throw new ReaderError("File is not a scene manager configuration file."); //$NON-NLS-1$
			}
			
			IXMLElement e_gsb = xr.getElement( e_mgr, E_GLOBAL_SCENE_BUTTON, false );
			if( e_gsb!=null ) {
				int sl = xr.getAttrInt( e_gsb,  A_FLAGS );
				Rectangle b = xr.getAttrRectangle( e_gsb, A_BOUNDS );
				GlobalSceneButton gsb = getGlobalSceneButton();
				gsb.dispose();
				//as a scene must exists, gsb must exists too
				gsb = new GlobalSceneButton( scene, sl );
				gsb.setBounds( b );
			}
			
			IXMLElement e_gat = xr.getElement( e_mgr, E_GLOBAL_ACTION_TOOLBAR, false );
			if( e_gat!=null ) {
				global_action_toolbar_location = xr.getAttrPoint( e_gat, A_LOCATION );
				GlobalActionToolBar gat = scene.getGlobalActionToolbar();
				if( gat!=null ) { //available only in edit mode
					gat.setLocation( global_action_toolbar_location );
				}
			}
			
			//any side button ?
			for( Object n : e_mgr.getChildren() ) {
				if( n instanceof IXMLElement ) {
					IXMLElement e = (IXMLElement)n;
					if( E_SIDE_BUTTON.equals( e.getName() ) ) {
						int sl = xr.getAttrInt( e, A_FLAGS );
						Rectangle bounds = xr.getAttrRectangle( e, A_BOUNDS );
						String action_id = xr.getAttrString( e, A_ACTION );
						IAction action = SideButtonActionFactory.getDefault().createAction( action_id );
						GlobalSceneSideActionButton ab = new GlobalSceneSideActionButton( scene, action, sl );
						ab.setBounds( bounds );
						ab.redraw();
					}
				}
			}
			
		} catch( IOException x ) {
			x.printStackTrace();
		} catch( IllegalAccessException x ) {
			x.printStackTrace();
		} catch( InstantiationException x ) {
			x.printStackTrace();
		} catch( ClassNotFoundException x ) {
			x.printStackTrace();
		} catch( XMLException x ) {
			x.printStackTrace();
		} catch( ReaderError x ) {
			x.printStackTrace();
		}
		finally {
			if( fis!=null ) {
				try {
					fis.close();
				} catch( IOException x ) {
					x.printStackTrace();
				}
			}
		}
	}
	
}
